package gui;

import gui.geometry.AddConnectionDescriptor;
import gui.geometry.GeometryDescriptor;
import gui.geometry.NetworkConnectionDescriptor;
import gui.geometry.NetworkNodeDescriptor;
import gui.geometry.TextDescriptor;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.NoninvertibleTransformException;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import model.*;
import model.NetworkConnection.Side;

public class NetworkView extends JPanel implements ViewUpdater {
	//--------------------------------------------------------------------------------------------------------------
	//--------------------------------------------------------------------------------------------------------------
	//Variables
	private static final long serialVersionUID = 1L;
	/**
	 * The model is used in drawing the connections as it is the only way to access the nodes.
	 */
	private NetworkModel model;
	
	/**
	 * The frame is used in drawing the background lines.
	 */
	private JFrame frame;
	
	/**
	 * The mouse is used to draw the mouse location snapped to the nearest shape.
	 */
	private Point mouse = null;
	
	/**
	 * The font metric is set in the paintComponent which will be called before it is ever used in
	 * pointGeometry. It will also be reset if the font ever changes.
	 */
	private FontMetrics fm;
	
	/**
	 * This is the selected object as determined by mouseReleased calling pointGeometry. If the mouse
	 * is released over any object it is saved. If released over no object, selectedObject should be
	 * null and should not try to draw itself.
	 */
	private GeometryDescriptor selectedObject;
	private AddConnectionDescriptor addConnectionDescriptor;
	private JLabel open;
	private JPanel buttons;
	private JButton select, addNode, addConnection, rotate, selectedControl;
	private DrawPanel drawPanel;
	private RotateController rotateController;
	private AffineTransform identity;
	
	//--------------------------------------------------------------------------------------------------------------
	//--------------------------------------------------------------------------------------------------------------
	//Constructors
	public NetworkView(JFrame frame) {
		this.frame = frame;
		
		initialize();
		open = new JLabel("Open a file...");
		drawPanel.add(open);
	}
	
	public void addModel(NetworkModel model) {
		this.model = model;
		setBackground(Paintbrush.white);
		model.addViewListener(this);
		updateView();
		drawPanel.setBackground(Paintbrush.white);
		drawPanel.remove(open);
		select.setSelected(true);
	}
	
	public void setModel(NetworkModel model) {
		this.model = model;
		model.addViewListener(this);
	}
	
	public NetworkView(NetworkModel model, JFrame frame) {
		this.model = model;
		this.frame = frame;
		setBackground(Paintbrush.white);
		
		initialize();
		select.setSelected(true);
		drawPanel.setBackground(Paintbrush.white);
		model.addViewListener(this);
	}
	
	private void initialize() {
		
		setFocusable(true);
		setFont(new Font("Sans Serif", Font.PLAIN, 14));
		
		setLayout(new BorderLayout());
		drawPanel = new DrawPanel();
		drawPanel.addMouseListener(new NetworkViewListener());
		drawPanel.addMouseMotionListener(new NetworkViewListener());
		addKeyListener(new NetworkViewKeyListener());
		
		add(drawPanel, BorderLayout.CENTER);
		buttons = new JPanel();
		buttons.setLayout(new GridLayout(12, 1));
		
		ButtonListener buttonListener = new ButtonListener();
		select = new JButton(new ImageIcon("select.png"));
		select.addActionListener(buttonListener);
		buttons.add(select);
		selectedControl = select;
		
		addNode = new JButton(new ImageIcon("addNode.png"));
		addNode.addActionListener(buttonListener);
		buttons.add(addNode);
		
		addConnection = new JButton(new ImageIcon("addConnection.png"));
		addConnection.addActionListener(buttonListener);
		buttons.add(addConnection);
		
		rotate = new JButton(new ImageIcon("rotate.png"));
		rotate.addActionListener(buttonListener);
		buttons.add(rotate);

		buttons.setBorder(BorderFactory.createRaisedBevelBorder());
		add(buttons, BorderLayout.WEST);
		rotateController = new RotateController();
	}
	
	//--------------------------------------------------------------------------------------------------------------
	//--------------------------------------------------------------------------------------------------------------
	//Methods
	
	/**
	 * Determines the end point for a connection.
	 * @return the x and y value for the endpoint
	 */
	public static int [] getEndPoint(NetworkNode node, Side side, FontMetrics fm) {
		int [] point = new int[2];
		
		int w = fm.stringWidth(node.getName());
		int width = ((int)(w / 2 * 1.2) / 10 + 1) * 10;
		int height = ((int)(w / 2 * .5) / 10 + 1) * 10;
		
		switch(side) {
		case Left:
			point[0] = (int) (node.getX() - width);
			point[1] = (int) node.getY();
			break;
		case Right:
			point[0] = (int) (node.getX() + width);
			point[1] = (int) node.getY();
			break;
		case Top:
			point[0] = (int) (node.getX());
			point[1] = (int) (node.getY() - height);
			break;
		case Bottom:
			point[0] = (int) (node.getX());
			point[1] = (int) (node.getY() + height);
			break;
		}
		return point;
	}
	
	//----------------------------------------
	/**
	 * Determines if the mouse click is inside any shapes.
	 * @return A geometryDescriptor that includes the shape selected or null if no shape was selected
	 */
	public GeometryDescriptor pointGeometry(Point point) {
		if(model == null) {
			return null;
		}
		GeometryDescriptor descriptor = null;
		find: {
			for(int i = model.nNodes() - 1; i >= 0; i--) {
				descriptor = NetworkNodeDescriptor.contains(point, model.getNode(i), fm);
				if(descriptor != null) {
					if(descriptor.getIndex() == -1) {
						descriptor.setIndex(i);
					}
					break find;
				}
			}
			for(int i = model.nConnections() - 1; i >= 0; i--) {
				descriptor = NetworkConnectionDescriptor.contains(point, model.getConnection(i), fm);
				if(descriptor != null) {
					descriptor.setIndex(i);
					break find;
				}
			}
		}
		return descriptor;
	}
	
	public NetworkModel getModel() {
		return model;
	}
	
	//----------------------------------------
	/**
	 * This method repaints the view. After careful thought, I have concluded that until
	 * the program begins to lag, the best option is to repaint the whole view. When a node
	 * changes size or position, all connections to that node must also be redrawn. This
	 * will likely result in requiring the view to partially redraw other nodes. Redrawing
	 * partial ovals seems to be difficult for the view and results in a severe case of
	 * the jaggies.
	 */
	public void updateView() {
		repaint();
	}
	
	//----------------------------------------
	private class DrawPanel extends JPanel {
		private static final long serialVersionUID = 1L;
		
		public void paintComponent(Graphics graphic) {
			super.paintComponent(graphic);
			
			if(model == null) {
				return;
			}
			
			Graphics2D g = (Graphics2D)graphic;
			
			identity = g.getTransform();
			g.setColor(Paintbrush.grey);
			int frameWidth = frame.getWidth();
			int frameHeight = frame.getHeight();
			for(int i = 1; i <= frameWidth / 10; i++) {
				g.draw(new Line2D.Double(i * 10, 0, i * 10, frameHeight));
			}
			for(int i = 1; i <= frameHeight / 10; i++) {
				g.draw(new Line2D.Double(0, i * 10, frameWidth, i * 10));
			}
			
			
			g.transform(rotateController.getTransform());
			
			fm = g.getFontMetrics();
			int h = fm.getHeight() - 5;
//			int h = fm.getHeight() - 9;
			
			g.setColor(Paintbrush.darkGrey);
			for(int i = 0; i < model.nNodes(); i++) {
				NetworkNode node = model.getNode(i);
				int w = fm.stringWidth(node.getName());
				int width = ((int)(w / 2 * 1.2) / 10 + 1) * 10;
				int height = ((int)(w / 2 * .5) / 10 + 1) * 10;
				
				Shape shape = new Ellipse2D.Double((int)(node.getX() - width) + 2, (int)(node.getY() - height) + 3, width * 2, height * 2);
				g.fill(shape);
//				g.fillOval((int)(node.getX() - width) + 2, (int)(node.getY() - height) + 3, width * 2, height * 2);
			}
			
			g.setColor(Paintbrush.navy);
			for(int i = 0; i < model.nConnections(); i++) {
				NetworkConnection n = model.getConnection(i);
				int [] begin = getEndPoint(n.node1, n.side1, fm);
				int [] end = getEndPoint(n.node2, n.side2, fm);
				int [] control = n.getControlPoints((int)Math.sqrt((begin[0] - end[0]) * (begin[0] - end[0]))
						+ (begin[1] - end[1]) * (begin[1] - end[1]));
//				g.draw(new Line2D.Double(begin[0], begin[1], end[0], end[1]));
	//			create new CubicCurve2D.Double
				CubicCurve2D c = new CubicCurve2D.Double();
				c.setCurve(begin[0], begin[1], begin[0] + control[0], begin[1] + control[1], 
						end[0] + control[2], end[1] + control[3], end[0], end[1]);
				g.draw(c);
				
			}
			
			for(int i = 0; i < model.nNodes(); i++) {
				NetworkNode node = model.getNode(i);
				int w = fm.stringWidth(node.getName());
				int width = ((int)(w / 2 * 1.2) / 10 + 1) * 10;
				int height = ((int)(w / 2 * .5) / 10 + 1) * 10;
				
				Shape shape = new Ellipse2D.Double((int)(node.getX() - width), (int)(node.getY() - height), width * 2, height * 2);
				g.setColor(Paintbrush.white);
				g.fill(shape);
//				g.fillOval((int)(node.getX() - width), (int)(node.getY() - height), width * 2, height * 2);
				g.setColor(Paintbrush.navy);
				g.draw(shape);
				//g.drawOval((int)(node.getX() - width), (int)(node.getY() - height), width * 2, height * 2);
				g.drawString(node.getName(), (int)(node.getX() - w / 2.0), (int)(node.getY() + h / 2.0));
			}
			
			if(mouse != null) {
				g.setColor(Paintbrush.seaFoam);
				g.setStroke(new BasicStroke(3));
				g.drawRect(mouse.x - 2, mouse.y - 2, 5, 5);
				g.setStroke(new BasicStroke(1));
			}
			
			if(selectedObject != null) {
				selectedObject.draw(g);
			}
			
			if(addConnectionDescriptor != null) {
				addConnectionDescriptor.draw(g);
			}
			
			if(selectedControl == rotate) {
				rotateController.draw(g);
			}
			g.setTransform(identity);
		}
	}
	
	//--------------------------------------------------------------------------------------------------------------
	//--------------------------------------------------------------------------------------------------------------
	//Controller
	
	private Point convertPoint(Point point) {
	
		Point result = point;
		try {
			rotateController.getTransform().createInverse().transform(point, result);
		}
		catch (NoninvertibleTransformException e) {
			e.printStackTrace();
		}
		return result;
	}
	
	//----------------------------------------
	/**
	 * Mouse listener used to determine the point geometry.
	 */
	private class NetworkViewListener extends MouseAdapter {
		public void mouseMoved(MouseEvent event) {
			Point point = convertPoint(event.getPoint());
			if(model == null) {
				return;
			}
			if(selectedControl == select) {
				GeometryDescriptor descriptor = pointGeometry(point);
				if(descriptor != null) {
					mouse = descriptor.getPoint();
				}
				else {
					mouse = null;
				}
				updateView();
			}
			
			if(selectedControl == addConnection) {
				AddConnectionDescriptor close = new AddConnectionDescriptor();
				mouse = null;
				for(int i = model.nNodes() - 1; i >= 0; i--) {
					Side side = close.contains(point, model.getNode(i), fm, true);
					if(side != null) {
						mouse = close.pressPoint;
						break;
					}
				}
				updateView();
			}
		}
		public void mousePressed(MouseEvent event) {
			Point point = convertPoint(event.getPoint());
			mouse = null;
			
			if(selectedControl == select) {
				selectedObject = pointGeometry(point);
				if(selectedObject instanceof NetworkNodeDescriptor) {
					((NetworkNodeDescriptor)selectedObject).setPressPoint(point);
				}
				updateView();
			}
			if(selectedControl == addConnection) {
				addConnectionDescriptor = new AddConnectionDescriptor();
				addConnectionDescriptor.setPressPoint(point, model, fm);
			}
			if(selectedControl == rotate) {
				rotateController.setPressPoint(point);
			}
		}
		public void mouseDragged(MouseEvent event) {
			Point point = convertPoint(event.getPoint());
			if(selectedControl == select) {
				if(selectedObject instanceof NetworkNodeDescriptor) {
					((NetworkNodeDescriptor)selectedObject).setDragPoint(point);
				}
			}
			if(selectedControl == addConnection) {
				addConnectionDescriptor.setDragPoint(point, model, fm);
				updateView();
			}
			if(selectedControl == rotate) {
				if(rotateController.isClicked()) {
					rotateController.setDragPoint(point);
				}
				else {
					rotateController.createTransform(point);
					updateView();
				}
			}
		}
		public void mouseReleased(MouseEvent event) {
			Point point = convertPoint(event.getPoint());
			if(selectedControl == addNode) {
				String nodeName = JOptionPane.showInputDialog("What would you like to name the node?");
				if(nodeName == null) {
					return;
				}
				NetworkNode newNode = new NetworkNode(nodeName, point.x, point.y);
				model.addNode(newNode);
				newNode.setNetwork(model);
			}
			if(selectedControl == select) {
				if(selectedObject instanceof NetworkNodeDescriptor) {
					((NetworkNodeDescriptor)selectedObject).setReleasePoint();
				}
			}
			if(selectedControl == addConnection) {
				addConnectionDescriptor.setReleasePoint(point, model, fm);
				addConnectionDescriptor = null;
			}
			if(selectedControl == rotate) {
				rotateController.setReleasePoint(point, event.getPoint());
				updateView();
			}
		}
	}
	
	private class NetworkViewKeyListener extends KeyAdapter {
		public void keyPressed(KeyEvent event) {
			if(selectedObject instanceof NetworkConnectionDescriptor) {
				if(event.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
					int index = selectedObject.index;
					selectedObject = null;
					model.removeConnection(index);
				}
			}
			if(selectedObject instanceof NetworkNodeDescriptor) {
				if(event.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
					int index = selectedObject.index;
					selectedObject = null;
					model.removeNode(index);
				}
			}
			
			if(selectedObject instanceof TextDescriptor) {
				char eventChar = event.getKeyChar();
				if(event.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
					((TextDescriptor)selectedObject).updateBackspace();
				}
				else if(event.getKeyCode() == KeyEvent.VK_LEFT) {
					((TextDescriptor)selectedObject).moveCursor(-1);
					//I am fully aware that I am updating the view here. This is necessary because the model is 
					//never changing, but the position of the cursor in only this view is changing.
					updateView();
				}
				else if(event.getKeyCode() == KeyEvent.VK_RIGHT) {
					((TextDescriptor)selectedObject).moveCursor(1);
					//I am fully aware that I am updating the view here. This is necessary because the model is 
					//never changing, but the position of the cursor in only this view is changing.
					updateView();
				}
				else if(eventChar >= 32 && eventChar <= 127 && '\"' != eventChar) {
					((TextDescriptor)selectedObject).typeCharacter(eventChar);
				}
			}
		}
	}

	private class ButtonListener implements ActionListener {
	
		@Override
		public void actionPerformed(ActionEvent event) {
			if(model != null) {
				select.setSelected(false);
				addNode.setSelected(false);
				addConnection.setSelected(false);
				rotate.setSelected(false);
				JButton source = (JButton)(event.getSource());
				source.setSelected(true);
				selectedControl = source;
				if(source != select) {
					selectedObject = null;
				}
				if(source == rotate) {
					updateView();
				}
			}
			requestFocusInWindow();
		}
	}
}